<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-CN">
<head>
  <title></title>
  <link href="../Styles/stylesheet.css" rel="stylesheet" type="text/css" />
  
<style type="text/css">
@page { margin-bottom: 5.000000pt; margin-top: 5.000000pt; }
</style>
</head>

<body>
  <h2><span style="border-bottom:1px solid">Chapter_29</span></h2>

  <p>l.setText(</p>

  <p>"Name must preceed '@' in email name");</p>

  <p>l2.setText("");</p>

  <p>return true;</p>

  <p>}</p>

  <p>String end =</p>

  <p>str.substring(str.indexOf('@'));</p>

  <p>if(end.indexOf('.') == -1) {</p>

  <p>l.setText("Portion after '@' must " +</p>

  <p>"have an extension, such as '.com'");</p>

  <p>l2.setText("");</p>

  <p>return true;</p>

  <p>}</p>

  <p>// Build and encode the email data:</p>

  <p>String emailData =</p>

  <p>"name=" + URLEncoder.encode(</p>

  <p>name.getText().trim()) +</p>

  <p>"&amp;email=" + URLEncoder.encode(</p>

  <p>email.getText().trim().toLowerCase()) +</p>

  <p>"&amp;submit=Submit";</p>

  <p>// Send the name using CGI's GET process:</p>

  <p>try {</p>

  <p>l.setText("Sending...");</p>

  <p>URL u = new URL(</p>

  <p>getDocumentBase(), "cgi-bin/" +</p>

  <p>CGIProgram + "?" + emailData);</p>

  <p>l.setText("Sent: " + email.getText());</p>

  <p>send.setLabel("Re-send");</p>

  <p>l2.setText(</p>

  <p>"Waiting for reply " + ++vcount);</p>

  <p>DataInputStream server =</p>

  <p>new DataInputStream(u.openStream());</p>

  <p>String line;</p>

  <p>while((line = server.readLine()) != null)</p>

  <p>l2.setText(line);</p>

  <p>} catch(MalformedURLException e) {</p>

  <p>l.setText("Bad URl");</p>

  <p>} catch(IOException e) {</p>

  <p>l.setText("IO Exception");</p>

  <p>}</p>

  <p>}</p>

  <p>else return super.action(evt, arg);</p>

  <p>return true;</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>CGI程序（不久即可看到）的名字是Listmgr2.exe。许多Web服务器都在Unix机器上运行（Linux也越来越受到青睐）。根据传统，它们一般不为自己的可执行程序采用.exe扩展名。但在Unix操作系统中，可以把自己的程序称呼为自己希望的任何东西。若使用的是.exe扩展名，程序毋需任何修改即可通过Unix和Win32的运行测试。</p>

  <p>和往常一样，程序片设置了自己的用户界面（这次是两个输入字段，不是一个）。唯一显著的区别是在action()方法内产生的。该方法的作用是对按钮按下事件进行控制。名字检查过以后，大家会发现下述代码行：</p>

  <p>String emailData =</p>

  <p>"name=" + URLEncoder.encode(</p>

  <p>name.getText().trim()) +</p>

  <p>"&amp;email=" + URLEncoder.encode(</p>

  <p>email.getText().trim().toLowerCase()) +</p>

  <p>"&amp;submit=Submit";</p>

  <p>// Send the name using CGI's GET process:</p>

  <p>try {</p>

  <p>l.setText("Sending...");</p>

  <p>URL u = new URL(</p>

  <p>getDocumentBase(), "cgi-bin/" +</p>

  <p>CGIProgram + "?" + emailData);</p>

  <p>l.setText("Sent: " + email.getText());</p>

  <p>send.setLabel("Re-send");</p>

  <p>l2.setText(</p>

  <p>"Waiting for reply " + ++vcount);</p>

  <p>DataInputStream server =</p>

  <p>new DataInputStream(u.openStream());</p>

  <p>String line;</p>

  <p>while((line = server.readLine()) != null)</p>

  <p>l2.setText(line);</p>

  <p>// ...</p>

  <p>name和email数据都是它们对应的文字框里提取出来，而且两端多余的空格都用trim()剔去了。为了进入列表，email名字被强制换成小写形式，以便能够准确地对比（防止基于大小写形式的错误判断）。来自每个字段的数据都编码为URL形式，随后采用与HTML页中一样的方式汇编GET字串（这样一来，我们可将Java程序片与现有的任何CGI程序结合使用，以满足常规的HTML GET请求）。</p>

  <p>到这时，一些Java的魔力已经开始发挥作用了：如果想同任何URL连接，只需创建一个URL对象，并将地址传递给构建器即可。构建器会负责建立同服务器的连接（对Web服务器来说，所有连接行动都是根据作为URL使用的字串来判断的）。就目前这种情况来说，URL指向的是当前Web站点的cgi-bin目录（当前Web站点的基础地址是用getDocumentBase()设定的）。一旦Web服务器在URL中看到了一个“cgi-bin”，会接着希望在它后面跟随了cgi-bin目录内的某个程序的名字，那是我们要运行的目标程序。程序名后面是一个问号以及CGI程序会在QUERY_STRING环境变量中查找的一个参数字串（马上就要学到）。</p>

  <p>我们发出任何形式的请求后，一般都会得到一个回应的HTML页。但若使用Java的URL对象，我们可以拦截自CGI程序传回的任何东西，只需从URL对象里取得一个InputStream（输入数据流）即可。这是用URL对象的openStream()方法实现，它要封装到一个DataInputStream里。随后就可以读取数据行，若readLine()返回一个null（空值），就表明CGI程序已结束了它的输出。</p>

  <p>我们即将看到的CGI程序返回的仅仅是一行，它是用于标志成功与否（以及失败的具体原因）的一个字串。这一行会被捕获并置放第二个Label字段里，使用户看到具体发生了什么事情。</p>

  <p>1. 从程序片里显示一个Web页</p>

  <p>程序亦可将CGI程序的结果作为一个Web页显示出来，就象它们在普通HTML模式中运行那样。可用下述代码做到这一点：</p>

  <p>getAppletContext().showDocument(u);</p>

  <p>其中，u代表URL对象。这是将我们重新定向于另一个Web页的一个简单例子。那个页凑巧是一个CGI程序的输出，但可以非常方便地进入一个原始的HTML页，所以可以构建这个程序片，令其产生一个由密码保护的网关，通过它进入自己Web站点的特殊部分：</p>

  <p>//: ShowHTML.java</p>

  <p>import java.awt.*;</p>

  <p>import java.applet.*;</p>

  <p>import java.net.*;</p>

  <p>import java.io.*;</p>

  <p>public class ShowHTML extends Applet {</p>

  <p>static final String CGIProgram = "MyCGIProgram";</p>

  <p>Button send = new Button("Go");</p>

  <p>Label l = new Label();</p>

  <p>public void init() {</p>

  <p>add(send);</p>

  <p>add(l);</p>

  <p>}</p>

  <p>public boolean action (Event evt, Object arg) {</p>

  <p>if(evt.target.equals(send)) {</p>

  <p>try {</p>

  <p>// This could be an HTML page instead of</p>

  <p>// a CGI program. Notice that this CGI</p>

  <p>// program doesn't use arguments, but</p>

  <p>// you can add them in the usual way.</p>

  <p>URL u = new URL(</p>

  <p>getDocumentBase(),</p>

  <p>"cgi-bin/" + CGIProgram);</p>

  <p>// Display the output of the URL using</p>

  <p>// the Web browser, as an ordinary page:</p>

  <p>getAppletContext().showDocument(u);</p>

  <p>} catch(Exception e) {</p>

  <p>l.setText(e.toString());</p>

  <p>}</p>

  <p>}</p>

  <p>else return super.action(evt, arg);</p>

  <p>return true;</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>URL类的最大的特点就是有效地保护了我们的安全。可以同一个Web服务器建立连接，毋需知道幕后的任何东西。</p>

  <p>15.6.3 用C++写的CGI程序</p>

  <p>经过前面的学习，大家应该能够根据例子用ANSI C为自己的服务器写出CGI程序。之所以选用ANSI C，是因为它几乎随处可见，是最流行的C语言标准。当然，现在的C++也非常流行了，特别是采用GNU C++编译器（g++）形式的那一些（注释④）。可从网上许多地方免费下载g++，而且可选用几乎所有平台的版本（通常与Linux那样的操作系统配套提供，且已预先安装好）。正如大家即将看到的那样，从CGI程序可获得面向对象程序设计的许多好处。</p>

  <p>④：GNU的全称是“Gnu's Not Unix”。这最早是由“自由软件基金会”（FSF）负责开发的一个项目，致力于用一个免费的版本取代原有的Unix操作系统。现在的Linux似乎正在做前人没有做到的事情。但GNU工具在Linux的开发中扮演了至关重要的角色。事实上，Linux的整套软件包附带了数量非常多的GNU组件。</p>

  <p>为避免第一次就提出过多的新概念，这个程序并未打算成为一个“纯”C++程序；有些代码是用普通C写成的――尽管还可选用C++的一些替用形式。但这并不是个突出的问题，因为该程序用C++制作最大的好处就是能够创建类。在解析CGI信息的时候，由于我们最关心的是字段的“名称／值”对，所以要用一个类（Pair）来代表单个名称／值对；另一个类（CGI_vector）则将CGI字串自动解析到它会容纳的Pair对象里（作为一个vector），这样即可在有空的时候把每个Pair（对）都取出来。</p>

  <p>这个程序同时也非常有趣，因为它演示了C++与Java相比的许多优缺点。大家会看到一些相似的东西；比如class关键字。访问控制使用的是完全相同的关键字public和private，但用法却有所不同。它们控制的是一个块，而非单个方法或字段（也就是说，如果指定private:，后续的每个定义都具有private属性，直到我们再指定public:为止）。另外在创建一个类的时候，所有定义都自动默认为private。</p>

  <p>在这儿使用C++的一个原因是要利用C++“标准模板库”（STL）提供的便利。至少，STL包含了一个vector类。这是一个C++模板，可在编译期间进行配置，令其只容纳一种特定类型的对象（这里是Pair对象）。和Java的Vector不同，如果我们试图将除Pair对象之外的任何东西置入vector，C++的vector模板都会造成一个编译期错误；而Java的Vector能够照单全收。而且从vector里取出什么东西的时候，它会自动成为一个Pair对象，毋需进行造型处理。所以检查在编译期进行，这使程序显得更为“健壮”。此外，程序的运行速度也可以加快，因为没有必要进行运行期间的造型。vector也会过载operator[]，所以可以利用非常方便的语法来提取Pair对象。vector模板将在CGI_vector创建时使用；在那时，大家就可以体会到如此简短的一个定义居然蕴藏有那么巨大的能量。</p>

  <p>若提到缺点，就一定不要忘记Pair在下列代码中定义时的复杂程度。与我们在Java代码中看到的相比，Pair的方法定义要多得多。这是由于C++的程序员必须提前知道如何用副本构建器控制复制过程，而且要用过载的operator=完成赋值。正如第12章解释的那样，我们有时也要在Java中考虑同样的事情。但在C++中，几乎一刻都不能放松对这些问题的关注。</p>

  <p>这个项目首先创建一个可以重复使用的部分，由C++头文件中的Pair和CGI_vector构成。从技术角度看，确实不应把这些东西都塞到一个头文件里。但就目前的例子来说，这样做不会造成任何方面的损害，而且更具有Java风格，所以大家阅读理解代码时要显得轻松一些：</p>

  <p>//: CGITools.h</p>

  <p>// Automatically extracts and decodes data</p>

  <p>// from CGI GETs and POSTs. Tested with GNU C++</p>

  <p>// (available for most server machines).</p>

  <p>#include &lt;string.h&gt;</p>

  <p>#include &lt;vector&gt; // STL vector</p>

  <p>using namespace std;</p>

  <p>// A class to hold a single name-value pair from</p>

  <p>// a CGI query. CGI_vector holds Pair objects and</p>

  <p>// returns them from its operator[].</p>

  <p>class Pair {</p>

  <p>char* nm;</p>

  <p>char* val;</p>

  <p>public:</p>

  <p>Pair() { nm = val = 0; }</p>

  <p>Pair(char* name, char* value) {</p>

  <p>// Creates new memory:</p>

  <p>nm = decodeURLString(name);</p>

  <p>val = decodeURLString(value);</p>

  <p>}</p>

  <p>const char* name() const { return nm; }</p>

  <p>const char* value() const { return val; }</p>

  <p>// Test for "emptiness"</p>

  <p>bool empty() const {</p>

  <p>return (nm == 0) || (val == 0);</p>

  <p>}</p>

  <p>// Automatic type conversion for boolean test:</p>

  <p>operator bool() const {</p>

  <p>return (nm != 0) &amp;&amp; (val != 0);</p>

  <p>}</p>

  <p>// The following constructors &amp; destructor are</p>

  <p>// necessary for bookkeeping in C++.</p>

  <p>// Copy-constructor:</p>

  <p>Pair(const Pair&amp; p) {</p>

  <p>if(p.nm == 0 || p.val == 0) {</p>

  <p>nm = val = 0;</p>

  <p>} else {</p>

  <p>// Create storage &amp; copy rhs values:</p>

  <p>nm = new char[strlen(p.nm) + 1];</p>

  <p>strcpy(nm, p.nm);</p>

  <p>val = new char[strlen(p.val) + 1];</p>

  <p>strcpy(val, p.val);</p>

  <p>}</p>

  <p>}</p>

  <p>// Assignment operator:</p>

  <p>Pair&amp; operator=(const Pair&amp; p) {</p>

  <p>// Clean up old lvalues:</p>

  <p>delete nm;</p>

  <p>delete val;</p>

  <p>if(p.nm == 0 || p.val == 0) {</p>

  <p>nm = val = 0;</p>

  <p>} else {</p>

  <p>// Create storage &amp; copy rhs values:</p>

  <p>nm = new char[strlen(p.nm) + 1];</p>

  <p>strcpy(nm, p.nm);</p>

  <p>val = new char[strlen(p.val) + 1];</p>

  <p>strcpy(val, p.val);</p>

  <p>}</p>

  <p>return *this;</p>

  <p>}</p>

  <p>~Pair() { // Destructor</p>

  <p>delete nm; // 0 value OK</p>

  <p>delete val;</p>

  <p>}</p>

  <p>// If you use this method outide this class,</p>

  <p>// you're responsible for calling 'delete' on</p>

  <p>// the pointer that's returned:</p>

  <p>static char*</p>

  <p>decodeURLString(const char* URLstr) {</p>

  <p>int len = strlen(URLstr);</p>

  <p>char* result = new char[len + 1];</p>

  <p>memset(result, len + 1, 0);</p>

  <p>for(int i = 0, j = 0; i &lt;= len; i++, j++) {</p>

  <p>if(URLstr[i] == '+')</p>

  <p>result[j] = ' ';</p>

  <p>else if(URLstr[i] == '%') {</p>

  <p>result[j] =</p>

  <p>translateHex(URLstr[i + 1]) * 16 +</p>

  <p>translateHex(URLstr[i + 2]);</p>

  <p>i += 2; // Move past hex code</p>

  <p>} else // An ordinary character</p>

  <p>result[j] = URLstr[i];</p>

  <p>}</p>

  <p>return result;</p>

  <p>}</p>

  <p>// Translate a single hex character; used by</p>

  <p>// decodeURLString():</p>

  <p>static char translateHex(char hex) {</p>

  <p>if(hex &gt;= 'A')</p>

  <p>return (hex &amp; 0xdf) - 'A' + 10;</p>

  <p>else</p>

  <p>return hex - '0';</p>

  <p>}</p>

  <p>};</p>

  <p>// Parses any CGI query and turns it</p>

  <p>// into an STL vector of Pair objects:</p>

  <p>class CGI_vector : public vector&lt;Pair&gt; {</p>

  <p>char* qry;</p>

  <p>const char* start; // Save starting position</p>

  <p>// Prevent assignment and copy-construction:</p>

  <p>void operator=(CGI_vector&amp;);</p>

  <p>CGI_vector(CGI_vector&amp;);</p>

  <p>public:</p>

  <p>// const fields must be initialized in the C++</p>

  <p>// "Constructor initializer list":</p>

  <p>CGI_vector(char* query) :</p>

  <p>start(new char[strlen(query) + 1]) {</p>

  <p>qry = (char*)start; // Cast to non-const</p>

  <p>strcpy(qry, query);</p>

  <p>Pair p;</p>

  <p>while((p = nextPair()) != 0)</p>

  <p>push_back(p);</p>

  <p>}</p>

  <p>// Destructor:</p>

  <p>~CGI_vector() { delete start; }</p>

  <p>private:</p>

  <p>// Produces name-value pairs from the query</p>

  <p>// string. Returns an empty Pair when there's</p>

  <p>// no more query string left:</p>

  <p>Pair nextPair() {</p>

  <p>char* name = qry;</p>

  <p>if(name == 0 || *name == '\0')</p>

  <p>return Pair(); // End, return null Pair</p>

  <p>char* value = strchr(name, '=');</p>

  <p>if(value == 0)</p>

  <p>return Pair(); // Error, return null Pair</p>

  <p>// Null-terminate name, move value to start</p>

  <p>// of its set of characters:</p>

  <p>*value = '\0';</p>

  <p>value++;</p>

  <p>// Look for end of value, marked by '&amp;':</p>

  <p>qry = strchr(value, '&amp;');</p>

  <p>if(qry == 0) qry = ""; // Last pair found</p>

  <p>else {</p>

  <p>*qry = '\0'; // Terminate value string</p>

  <p>qry++; // Move to next pair</p>

  <p>}</p>

  <p>return Pair(name, value);</p>

  <p>}</p>

  <p>}; ///:~</p>

  <p>在#include语句后，可看到有一行是：</p>

  <p>using namespace std;</p>

  <p>C++中的“命名空间”（Namespace）解决了由Java的package负责的一个问题：将库名隐藏起来。std命名空间引用的是标准C++库，而vector就在这个库中，所以这一行是必需的。</p>

  <p>Pair类表面看异常简单，只是容纳了两个（private）字符指针而已――一个用于名字，另一个用于值。默认构建器将这两个指针简单地设为零。这是由于在C++中，对象的内存不会自动置零。第二个构建器调用方法decodeURLString()，在新分配的堆内存中生成一个解码过后的字串。这个内存区域必须由对象负责管理及清除，这与“破坏器”中见到的相同。name()和value()方法为相关的字段产生只读指针。利用empty()方法，我们查询Pair对象它的某个字段是否为空；返回的结果是一个bool――C++内建的基本布尔数据类型。operator bool()使用的是C++“运算符过载”的一种特殊形式。它允许我们控制自动类型转换。如果有一个名为p的Pair对象，而且在一个本来希望是布尔结果的表达式中使用，比如if(p){//...，那么编译器能辨别出它有一个Pair，而且需要的是个布尔值，所以自动调用operator bool()，进行必要的转换。</p>

  <p>接下来的三个方法属于常规编码，在C++中创建类时必须用到它们。根据C++类采用的所谓“经典形式”，我们必须定义必要的“原始”构建器，以及一个副本构建器和赋值运算符――operator=（以及破坏器，用于清除内存）。之所以要作这样的定义，是由于编译器会“默默”地调用它们。在对象传入、传出一个函数的时候，需要调用副本构建器；而在分配对象时，需要调用赋值运算符。只有真正掌握了副本构建器和赋值运算符的工作原理，才能在C++里写出真正“健壮”的类，但这需要需要一个比较艰苦的过程（注释⑤）。</p>

  <p>⑤：我的《Thinking in C++》（Prentice-Hall,1995）用了一整章的地方来讨论这个主题。若需更多的帮助，请务必看看那一章。</p>

  <p>只要将一个对象按值传入或传出函数，就会自动调用副本构建器Pair(const Pair&amp;)。也就是说，对于准备为其制作一个完整副本的那个对象，我们不准备在函数框架中传递它的地址。这并不是Java提供的一个选项，由于我们只能传递句柄，所以在Java里没有所谓的副本构建器（如果想制作一个本地副本，可以“克隆”那个对象――使用clone()，参见第12章）。类似地，如果在Java里分配一个句柄，它会简单地复制。但C++中的赋值意味着整个对象都会复制。在副本构建器中，我们创建新的存储空间，并复制原始数据。但对于赋值运算符，我们必须在分配新存储空间之前释放老存储空间。我们要见到的也许是C++类最复杂的一种情况，但那正是Java的支持者们论证Java比C++简单得多的有力证据。在Java中，我们可以自由传递句柄，善后工作则由垃圾收集器负责，所以可以轻松许多。</p>

  <p>但事情并没有完。Pair类为nm和val使用的是char*，最复杂的情况主要是围绕指针展开的。如果用较时髦的C++ string类来代替char*，事情就要变得简单得多（当然，并不是所有编译器都提供了对string的支持）。那么，Pair的第一部分看起来就象下面这样：</p>

  <p>class Pair {</p>

  <p>string nm;</p>

  <p>string val;</p>

  <p>public:</p>

  <p>Pair() { }</p>

  <p>Pair(char* name, char* value) {</p>

  <p>nm = decodeURLString(name);</p>

  <p>val = decodeURLString(value);</p>

  <p>}</p>

  <p>const char* name() const { return nm.c_str(); }</p>

  <p>const char* value() const {</p>

  <p>return val.c_str();</p>

  <p>}</p>

  <p>// Test for "emptiness"</p>

  <p>bool empty() const {</p>

  <p>return (nm.length() == 0)</p>

  <p>|| (val.length() == 0);</p>

  <p>}</p>

  <p>// Automatic type conversion for boolean test:</p>

  <p>operator bool() const {</p>

  <p>return (nm.length() != 0)</p>

  <p>&amp;&amp; (val.length() != 0);</p>

  <p>}</p>

  <p>（此外，对这个类decodeURLString()会返回一个string，而不是一个char*）。我们不必定义副本构建器、operator=或者破坏器，因为编译器已帮我们做了，而且做得非常好。但即使有些事情是自动进行的，C++程序员也必须了解副本构建以及赋值的细节。</p>

  <p>Pair类剩下的部分由两个方法构成：decodeURLString()以及一个“帮助器”方法translateHex()――将由decodeURLString()使用。注意translateHex()并不能防范用户的恶意输入，比如“%1H”。分配好足够的存储空间后（必须由破坏器释放），decodeURLString()就会其中遍历，将所有“+”都换成一个空格；将所有十六进制代码（以一个“%”打头）换成对应的字符。</p>

  <p>CGI_vector用于解析和容纳整个CGI GET命令。它是从STL vector里继承的，后者例示为容纳Pair。C++中的继承是用一个冒号表示，在Java中则要用extends。此外，继承默认为private属性，所以几乎肯定需要用到public关键字，就象这样做的那样。大家也会发现CGI_vector有一个副本构建器以及一个operator=，但它们都声明成private。这样做是为了防止编译器同步两个函数（如果不自己声明它们，两者就会同步）。但这同时也禁止了客户程序员按值或者通过赋值传递一个CGI_vector。</p>

  <p>CGI_vector的工作是获取QUERY_STRING，并把它解析成“名称／值”对，这需要在Pair的帮助下完成。它首先将字串复制到本地分配的内存，并用常数指针start跟踪起始地址（稍后会在破坏器中用于释放内存）。随后，它用自己的nextPair()方法将字串解析成原始的“名称／值”对，各个对之间用一个“=”和“&amp;”符号分隔。这些对由nextPair()传递给Pair构建器，所以nextPair()返回的是一个Pair对象。随后用push_back()将该对象加入vector。nextPair()遍历完整个QUERY_STRING后，会返回一个零值。</p>

  <p>现在基本工具已定义好，它们可以简单地在一个CGI程序中使用，就象下面这样：</p>

  <p>//: Listmgr2.cpp</p>

  <p>// CGI version of Listmgr.c in C++, which</p>

  <p>// extracts its input via the GET submission</p>

  <p>// from the associated applet. Also works as</p>

  <p>// an ordinary CGI program with HTML forms.</p>

  <p>#include &lt;stdio.h&gt;</p>

  <p>#include "CGITools.h"</p>

  <p>const char* dataFile = "list2.txt";</p>

  <p>const char* notify = "Bruce@EckelObjects.com";</p>

  <p>#undef DEBUG</p>

  <p>// Similar code as before, except that it looks</p>

  <p>// for the email name inside of '&lt;&gt;':</p>

  <p>int inList(FILE* list, const char* emailName) {</p>

  <p>const int BSIZE = 255;</p>

  <p>char lbuf[BSIZE];</p>

  <p>char emname[BSIZE];</p>

  <p>// Put the email name in '&lt;&gt;' so there's no</p>

  <p>// possibility of a match within another name:</p>

  <p>sprintf(emname, "&lt;%s&gt;", emailName);</p>

  <p>// Go to the beginning of the list:</p>

  <p>fseek(list, 0, SEEK_SET);</p>

  <p>// Read each line in the list:</p>

  <p>while(fgets(lbuf, BSIZE, list)) {</p>

  <p>// Strip off the newline:</p>

  <p>char * newline = strchr(lbuf, '\n');</p>

  <p>if(newline != 0)</p>

  <p>*newline = '\0';</p>

  <p>if(strstr(lbuf, emname) != 0)</p>

  <p>return 1;</p>

  <p>}</p>

  <p>return 0;</p>

  <p>}</p>

  <p>void main() {</p>

  <p>// You MUST print this out, otherwise the</p>

  <p>// server will not send the response:</p>

  <p>printf("Content-type: text/plain\n\n");</p>

  <p>FILE* list = fopen(dataFile, "a+t");</p>

  <p>if(list == 0) {</p>

  <p>printf("error: could not open database. ");</p>

  <p>printf("Notify %s", notify);</p>

  <p>return;</p>

  <p>}</p>

  <p>// For a CGI "GET," the server puts the data</p>

  <p>// in the environment variable QUERY_STRING:</p>

  <p>CGI_vector query(getenv("QUERY_STRING"));</p>

  <p>#if defined(DEBUG)</p>

  <p>// Test: dump all names and values</p>

  <p>for(int i = 0; i &lt; query.size(); i++) {</p>

  <p>printf("query[%d].name() = [%s], ",</p>

  <p>i, query[i].name());</p>

  <p>printf("query[%d].value() = [%s]\n",</p>

  <p>i, query[i].value());</p>

  <p>}</p>

  <p>#endif(DEBUG)</p>

  <p>Pair name = query[0];</p>

  <p>Pair email = query[1];</p>

  <p>if(name.empty() || email.empty()) {</p>

  <p>printf("error: null name or email");</p>

  <p>return;</p>

  <p>}</p>

  <p>if(inList(list, email.value())) {</p>

  <p>printf("Already in list: %s", email.value());</p>

  <p>return;</p>

  <p>}</p>

  <p>// It's not in the list, add it:</p>

  <p>fseek(list, 0, SEEK_END);</p>

  <p>fprintf(list, "%s &lt;%s&gt;;\n",</p>

  <p>name.value(), email.value());</p>

  <p>fflush(list);</p>

  <p>fclose(list);</p>

  <p>printf("%s &lt;%s&gt; added to list\n",</p>

  <p>name.value(), email.value());</p>

  <p>} ///:~</p>

  <p>alreadyInList()函数与前一个版本几乎是完全相同的，只是它假定所有电子函件地址都在一个“&lt;&gt;”内。</p>

  <p>在使用GET方法时（通过在FORM引导命令的METHOD标记内部设置，但这在这里由数据发送的方式控制），Web服务器会收集位于“?”后面的所有信息，并把它们置入环境变量QUERY_STRING（查询字串）里。所以为了读取那些信息，必须获得QUERY_STRING的值，这是用标准的C库函数getnv()完成的。在main()中，注意对QUERY_STRING的解析有多么容易：只需把它传递给用于CGI_vector对象的构建器（名为query），剩下的所有工作都会自动进行。从这时开始，我们就可以从query中取出名称和值，把它们当作数组看待（这是由于operator[]在vector里已经过载了）。在调试代码中，大家可看到这一切是如何运作的；调试代码封装在预处理器引导命令#if defined(DEBUG)和#endif(DEBUG)之间。</p>

  <p>现在，我们迫切需要掌握一些与CGI有关的东西。CGI程序用两个方式之一传递它们的输入：在GET执行期间通过QUERY_STRING传递（目前用的这种方式），或者在POST期间通过标准输入。但CGI程序通过标准输出发送自己的输出，这通常是用C程序的printf()命令实现的。那么这个输出到哪里去了呢？它回到了Web服务器，由服务器决定该如何处理它。服务器作出决定的依据是content-type（内容类型）头数据。这意味着假如content-type头不是它看到的第一件东西，就不知道该如何处理收到的数据。因此，我们无论如何也要使所有CGI程序都从content-type头开始输出。</p>

  <p>在目前这种情况下，我们希望服务器将所有信息都直接反馈回客户程序（亦即我们的程序片，它们正在等候给自己的回复）。信息应该原封不动，所以content-type设为text/plain（纯文本）。一旦服务器看到这个头，就会将所有字串都直接发还给客户。所以每个字串（三个用于出错条件，一个用于成功的加入）都会返回程序片。</p>

  <p>我们用相同的代码添加电子函件名称（用户的姓名）。但在CGI脚本的情况下，并不存在无限循环――程序只是简单地响应，然后就中断。每次有一个CGI请求抵达时，程序都会启动，对那个请求作出反应，然后自行关闭。所以CPU不可能陷入空等待的尴尬境地，只有启动程序和打开文件时才存在性能上的隐患。Web服务器对CGI请求进行控制时，它的开销会将这种隐患减轻到最低程度。</p>

  <p>这种设计的另一个好处是由于Pair和CGI_vector都得到了定义，大多数工作都帮我们自动完成了，所以只需修改main()即可轻松创建自己的CGI程序。尽管小服务程序（Servlet）最终会变得越来越流行，但为了创建快速的CGI程序，C++仍然显得非常方便。</p>

  <p>15.6.4 POST的概念</p>

  <p>在许多应用程序中使用GET都没有问题。但是，GET要求通过一个环境变量将自己的数据传递给CGI程序。但假如GET字串过长，有些Web服务器可能用光自己的环境空间（若字串长度超过200字符，就应开始关心这方面的问题）。CGI为此提供了一个解决方案：POST。通过POST，数据可以编码，并按与GET相同的方法连结起来。但POST利用标准输入将编码过后的查询字串传递给CGI程序。我们要做的全部事情就是判断查询字串的长度，而这个长度已在环境变量CONTENT_LENGTH中保存好了。一旦知道了长度，就可自由分配存储空间，并从标准输入中读入指定数量的字符。</p>

  <p>对一个用来控制POST的CGI程序，由CGITools.h提供的Pair和CGI_vector均可不加丝毫改变地使用。下面这段程序揭示了写这样的一个CGI程序有多么简单。这个例子将采用“纯”C++，所以studio.h库被iostream（IO数据流）代替。对于iostream，我们可以使用两个预先定义好的对象：cin，用于同标准输入连接；以及cout，用于同标准输出连接。有几个办法可从cin中读入数据以及向cout中写入。但下面这个程序准备采用标准方法：用“&lt;&lt;”将信息发给cout，并用一个成员函数（此时是read()）从cin中读入数据：</p>

  <p>//: POSTtest.cpp</p>

  <p>// CGI_vector works as easily with POST as it</p>

  <p>// does with GET. Written in "pure" C++.</p>

  <p>#include &lt;iostream.h&gt;</p>

  <p>#include "CGITools.h"</p>

  <p>void main() {</p>

  <p>cout &lt;&lt; "Content-type: text/plain\n" &lt;&lt; endl;</p>

  <p>// For a CGI "POST," the server puts the length</p>

  <p>// of the content string in the environment</p>

  <p>// variable CONTENT_LENGTH:</p>

  <p>char* clen = getenv("CONTENT_LENGTH");</p>

  <p>if(clen == 0) {</p>

  <p>cout &lt;&lt; "Zero CONTENT_LENGTH" &lt;&lt; endl;</p>

  <p>return;</p>

  <p>}</p>

  <p>int len = atoi(clen);</p>

  <p>char* query_str = new char[len + 1];</p>

  <p>cin.read(query_str, len);</p>

  <p>query_str[len] = '\0';</p>

  <p>CGI_vector query(query_str);</p>

  <p>// Test: dump all names and values</p>

  <p>for(int i = 0; i &lt; query.size(); i++)</p>

  <p>cout &lt;&lt; "query[" &lt;&lt; i &lt;&lt; "].name() = [" &lt;&lt;</p>

  <p>query[i].name() &lt;&lt; "], " &lt;&lt;</p>

  <p>"query[" &lt;&lt; i &lt;&lt; "].value() = [" &lt;&lt;</p>

  <p>query[i].value() &lt;&lt; "]" &lt;&lt; endl;</p>

  <p>delete query_str; // Release storage</p>

  <p>} ///:~</p>

  <p>getenv()函数返回指向一个字串的指针，那个字串指示着内容的长度。若指针为零，表明CONTENT_LENGTH环境变量尚未设置，所以肯定某个地方出了问题。否则就必须用ANSI C库函数atoi()将字串转换成一个整数。这个长度将与new一起运用，分配足够的存储空间，以便容纳查询字串（另加它的空中止符）。随后为cin()调用read()。read()函数需要取得指向目标缓冲区的一个指针以及要读入的字节数。随后用空字符（null）中止query_str，指出已经抵达字串的末尾，这就叫作“空中止”。</p>

  <p>到这个时候，我们得到的查询字串与GET查询字串已经没有什么区别，所以把它传递给用于CGI_vector的构建器。随后便和前例一样，我们可以自由vector内不同的字段。</p>

  <p>为测试这个程序，必须把它编译到主机Web服务器的cgi-bin目录下。然后就可以写一个简单的HTML页进行测试，就象下面这样：</p>

  <p>&lt;HTML&gt;</p>

  <p>&lt;HEAD&gt;</p>

  <p>&lt;META CONTENT="text/html"&gt;</p>

  <p>&lt;TITLE&gt;A test of standard HTML POST&lt;/TITLE&gt;</p>

  <p>&lt;/HEAD&gt;</p>

  <p>Test, uses standard html POST</p>

  <p>&lt;Form method="POST" ACTION="/cgi-bin/POSTtest"&gt;</p>

  <p>&lt;P&gt;Field1: &lt;INPUT TYPE = "text" NAME = "Field1"</p>

  <p>VALUE = "" size = "40"&gt;&lt;/p&gt;</p>

  <p>&lt;P&gt;Field2: &lt;INPUT TYPE = "text" NAME = "Field2"</p>

  <p>VALUE = "" size = "40"&gt;&lt;/p&gt;</p>

  <p>&lt;P&gt;Field3: &lt;INPUT TYPE = "text" NAME = "Field3"</p>

  <p>VALUE = "" size = "40"&gt;&lt;/p&gt;</p>

  <p>&lt;P&gt;Field4: &lt;INPUT TYPE = "text" NAME = "Field4"</p>

  <p>VALUE = "" size = "40"&gt;&lt;/p&gt;</p>

  <p>&lt;P&gt;Field5: &lt;INPUT TYPE = "text" NAME = "Field5"</p>

  <p>VALUE = "" size = "40"&gt;&lt;/p&gt;</p>

  <p>&lt;P&gt;Field6: &lt;INPUT TYPE = "text" NAME = "Field6"</p>

  <p>VALUE = "" size = "40"&gt;&lt;/p&gt;</p>

  <p>&lt;p&gt;&lt;input type = "submit" name = "submit" &gt; &lt;/p&gt;</p>

  <p>&lt;/Form&gt;</p>

  <p>&lt;/HTML&gt;</p>

  <p>填好这个表单并提交出去以后，会得到一个简单的文本页，其中包含了解析出来的结果。从中可知道CGI程序是否在正常工作。</p>

  <p>当然，用一个程序片来提交数据显得更有趣一些。然而，POST数据的提交属于一个不同的过程。在用常规方式调用了CGI程序以后，必须另行建立与服务器的一个连接，以便将查询字串反馈给它。服务器随后会进行一番处理，再通过标准输入将查询字串反馈回CGI程序。</p>

  <p>为建立与服务器的一个直接连接，必须取得自己创建的URL，然后调用openConnection()创建一个URLConnection。但是，由于URLConnection一般不允许我们把数据发给它，所以必须很可笑地调用setDoOutput(true)函数，同时调用的还包括setDoInput(true)以及setAllowUserInteraction(false)――注释⑥。最后，可调用getOutputStream()来创建一个OutputStream（输出数据流），并把它封装到一个DataOutputStream里，以便能按传统方式同它通信。下面列出的便是一个用于完成上述工作的程序片，必须在从它的各个字段里收集了数据之后再执行它：</p>

  <p>//: POSTtest.java</p>

  <p>// An applet that sends its data via a CGI POST</p>

  <p>import java.awt.*;</p>

  <p>import java.applet.*;</p>

  <p>import java.net.*;</p>

  <p>import java.io.*;</p>

  <p>public class POSTtest extends Applet {</p>

  <p>final static int SIZE = 10;</p>

  <p>Button submit = new Button("Submit");</p>

  <p>TextField[] t = new TextField[SIZE];</p>

  <p>String query = "";</p>

  <p>Label l = new Label();</p>

  <p>TextArea ta = new TextArea(15, 60);</p>

  <p>public void init() {</p>

  <p>Panel p = new Panel();</p>

  <p>p.setLayout(new GridLayout(t.length + 2, 2));</p>

  <p>for(int i = 0; i &lt; t.length; i++) {</p>

  <p>p.add(new Label(</p>

  <p>"Field " + i + " ", Label.RIGHT));</p>

  <p>p.add(t[i] = new TextField(30));</p>

  <p>}</p>

  <p>p.add(l);</p>

  <p>p.add(submit);</p>

  <p>add("North", p);</p>

  <p>add("South", ta);</p>

  <p>}</p>

  <p>public boolean action (Event evt, Object arg) {</p>

  <p>if(evt.target.equals(submit)) {</p>

  <p>query = "";</p>

  <p>ta.setText("");</p>

  <p>// Encode the query from the field data:</p>

  <p>for(int i = 0; i &lt; t.length; i++)</p>

  <p>query += "Field" + i + "=" +</p>

  <p>URLEncoder.encode(</p>

  <p>t[i].getText().trim()) +</p>

  <p>"&amp;";</p>

  <p>query += "submit=Submit";</p>

  <p>// Send the name using CGI's POST process:</p>

  <p>try {</p>

  <p>URL u = new URL(</p>

  <p>getDocumentBase(), "cgi-bin/POSTtest");</p>

  <p>URLConnection urlc = u.openConnection();</p>

  <p>urlc.setDoOutput(true);</p>

  <p>urlc.setDoInput(true);</p>

  <p>urlc.setAllowUserInteraction(false);</p>

  <p>DataOutputStream server =</p>

  <p>new DataOutputStream(</p>

  <p>urlc.getOutputStream());</p>

  <p>// Send the data</p>

  <p>server.writeBytes(query);</p>

  <p>server.close();</p>

  <p>// Read and display the response. You</p>

  <p>// cannot use</p>

  <p>// getAppletContext().showDocument(u);</p>

  <p>// to display the results as a Web page!</p>

  <p>DataInputStream in =</p>

  <p>new DataInputStream(</p>

  <p>urlc.getInputStream());</p>

  <p>String s;</p>

  <p>while((s = in.readLine()) != null) {</p>

  <p>ta.appendText(s + "\n");</p>

  <p>}</p>

  <p>in.close();</p>

  <p>}</p>

  <p>catch (Exception e) {</p>

  <p>l.setText(e.toString());</p>

  <p>}</p>

  <p>}</p>

  <p>else return super.action(evt, arg);</p>

  <p>return true;</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>⑥：我不得不说自己并没有真正理解这儿都发生了什么事情，这些概念都是从Elliotte Rusty Harold编著的《Java Network Programming》里得来的，该书由O'Reilly于1997年出版。他在书中提到了Java连网函数库中出现的许多令人迷惑的Bug。所以一旦涉足这些领域，事情就不是编写代码，然后让它自己运行那么简单。一定要警惕潜在的陷阱！</p>

  <p>信息发送到服务器后，我们调用getInputStream()，并把返回值封装到一个DataInputStream里，以便自己能读取结果。要注意的一件事情是结果以文本行的形式显示在一个TextArea（文本区域）中。为什么不简单地使用getAppletContext().showDocument(u)呢？事实上，这正是那些陷阱中的一个。上述代码可以很好地工作，但假如试图换用showDocument()，几乎一切都会停止运行。也就是说，showDocument()确实可以运行，但从POSTtest得到的返回结果是“Zero CONTENT_LENGTH”（内容长度为零）。所以不知道为什么原因，showDocument()阻止了POST查询向CGI程序的传递。我很难判断这到底是一个在以后版本里会修复的Bug，还是由于我的理解不够（我看过的书对此讲得都很模糊）。但无论在哪种情况下，只要能坚持在文本区域里观看自CGI程序返回的内容，上述程序片运行时就没有问题。</p>

  <p>15.7 用JDBC连接数据库</p>

  <p>据估算，将近一半的软件开发都要涉及客户（机）／服务器方面的操作。Java为自己保证的一项出色能力就是构建与平台无关的客户机／服务器数据库应用。在Java 1.1中，这一保证通过Java数据库连接（JDBC）实现了。</p>

  <p>数据库最主要的一个问题就是各家公司之间的规格大战。确实存在一种“标准”数据库语言，即“结构查询语言”（SQL-92），但通常都必须确切知道自己要和哪家数据库公司打交道，否则极易出问题，尽管存在所谓的“标准”。JDBC是面向“与平台无关”设计的，所以在编程的时候不必关心自己要使用的是什么数据库产品。然而，从JDBC里仍有可能发出对某些数据库公司专用功能的调用，所以仍然不可任性妄为。</p>

  <p>和Java中的许多API一样，JDBC也做到了尽量的简化。我们发出的方法调用对应于从数据库收集数据时想当然的做法：同数据库连接，创建一个语句并执行查询，然后处理结果集。</p>

  <p>为实现这一“与平台无关”的特点，JDBC为我们提供了一个“驱动程序管理器”，它能动态维护数据库查询所需的所有驱动程序对象。所以假如要连接由三家公司开发的不同种类的数据库，就需要三个单独的驱动程序对象。驱动程序对象会在装载时由“驱动程序管理器”自动注册，并可用Class.forName()强行装载。</p>

  <p>为打开一个数据库，必须创建一个“数据库URL”，它要指定下述三方面的内容：</p>

  <p>(1) 用“jdbc”指出要使用JDBC。</p>

  <p>(2) “子协议”：驱动程序的名字或者一种数据库连接机制的名称。由于JDBC的设计从ODBC吸收了许多灵感，所以可以选用的第一种子协议就是“jdbc-odbc桥”，它用“odbc”关键字即可指定。</p>

  <p>(3) 数据库标识符：随使用的数据库驱动程序的不同而变化，但一般都提供了一个比较符合逻辑的名称，由数据库管理软件映射（对应）到保存了数据表的一个物理目录。为使自己的数据库标识符具有任何含义，必须用自己的数据库管理软件为自己喜欢的名字注册（注册的具体过程又随运行平台的不同而变化）。</p>

  <p>所有这些信息都统一编译到一个字串里，即“数据库URL”。举个例子来说，若想通过ODBC子协议同一个标识为“people”的数据库连接，相应的数据库URL可设为：</p>

  <p>String dbUrl = "jdbc:odbc:people"</p>

  <p>如果通过一个网络连接，数据库URL也需要包含对远程机器进行标识的信息。</p>

  <p>准备好同数据库连接后，可调用静态方法DriverManager.getConnection()，将数据库的URL以及进入那个数据库所需的用户名密码传递给它。得到的返回结果是一个Connection对象，利用它即可查询和操纵数据库。</p>

  <p>下面这个例子将打开一个联络信息数据库，并根据命令行提供的参数查询一个人的姓（Last Name）。它只选择那些有E-mail地址的人的名字，然后列印出符合查询条件的所有人：</p>

  <p>//: Lookup.java</p>

  <p>// Looks up email addresses in a</p>

  <p>// local database using JDBC</p>

  <p>import java.sql.*;</p>

  <p>public class Lookup {</p>

  <p>public static void main(String[] args) {</p>

  <p>String dbUrl = "jdbc:odbc:people";</p>

  <p>String user = "";</p>

  <p>String password = "";</p>

  <p>try {</p>

  <p>// Load the driver (registers itself)</p>

  <p>Class.forName(</p>

  <p>"sun.jdbc.odbc.JdbcOdbcDriver");</p>

  <p>Connection c = DriverManager.getConnection(</p>

  <p>dbUrl, user, password);</p>

  <p>Statement s = c.createStatement();</p>

  <p>// SQL code:</p>

  <p>ResultSet r =</p>

  <p>s.executeQuery(</p>

  <p>"SELECT FIRST, LAST, EMAIL " +</p>

  <p>"FROM people.csv people " +</p>

  <p>"WHERE " +</p>

  <p>"(LAST='" + args[0] + "') " +</p>

  <p>" AND (EMAIL Is Not Null) " +</p>

  <p>"ORDER BY FIRST");</p>

  <p>while(r.next()) {</p>

  <p>// Capitalization doesn't matter:</p>

  <p>System.out.println(</p>

  <p>r.getString("Last") + ", "</p>

  <p>+ r.getString("fIRST")</p>

  <p>+ ": " + r.getString("EMAIL") );</p>

  <p>}</p>

  <p>s.close(); // Also closes ResultSet</p>

  <p>} catch(Exception e) {</p>

  <p>e.printStackTrace();</p>

  <p>}</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>可以看到，数据库URL的创建过程与我们前面讲述的完全一样。在该例中，数据库未设密码保护，所以用户名和密码都是空串。</p>

  <p>用DriverManager.getConnection()建好连接后，接下来可根据结果Connection对象创建一个Statement（语句）对象，这是用createStatement()方法实现的。根据结果Statement，我们可调用executeQuery()，向其传递包含了SQL-92标准SQL语句的一个字串（不久就会看到如何自动创建这类语句，所以没必要在这里知道关于SQL更多的东西）。</p>

  <p>executeQuery()方法会返回一个ResultSet（结果集）对象，它与继承器非常相似：next()方法将继承器移至语句中的下一条记录；如果已抵达结果集的末尾，则返回null。我们肯定能从executeQuery()返回一个ResultSet对象，即使查询结果是个空集（也就是说，不会产生一个违例）。注意在试图读取任何记录数据之前，都必须调用一次next()。若结果集为空，那么对next()的这个首次调用就会返回false。对于结果集中的每条记录，都可将字段名作为字串使用（当然还有其他方法），从而选择不同的字段。另外要注意的是字段名的大小写是无关紧要的――SQL数据库不在乎这个问题。为决定返回的类型，可调用getString()，getFloat()等等。到这个时候，我们已经用Java的原始格式得到了自己的数据库数据，接下去可用Java代码做自己想做的任何事情了。</p>

  <p>15.7.1 让示例运行起来</p>

  <p>就JDBC来说，代码本身是很容易理解的。最令人迷惑的部分是如何使它在自己特定的系统上运行起来。之所以会感到迷惑，是由于它要求我们掌握如何才能使JDBC驱动程序正确装载，以及如何用我们的数据库管理软件来设置一个数据库。</p>

  <p>当然，具体的操作过程在不同的机器上也会有所区别。但这儿提供的在32位Windows环境下操作过程可有效帮助大家理解在其他平台上的操作。</p>

  <p>1. 步骤1：寻找JDBC驱动程序</p>

  <p>上述程序包含了下面这条语句：</p>

  <p>Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");</p>

  <p>这似乎暗示着一个目录结构，但大家不要被它蒙骗了。在我手上这个JDK 1.1安装版本中，根本不存在叫作JdbcOdbcDriver.class的一个文件。所以假如在看了这个例子后去寻找它，那么必然会徒劳而返。另一些人提供的例子使用的是一个假名字，如“myDriver.ClassName”，但人们从字面上得不到任何帮助。事实上，上述用于装载jdbc-odbc驱动程序（实际是与JDK 1.1配套提供的唯一驱动）的语句在联机文档的多处地方均有出现（特别是在一个标记为“JDBC-ODBC Bridge Driver”的页内）。若上面的装载语句不能工作，那么它的名字可能已随着Java新版本的发布而改变了；此时应到联机文档里寻找新的表述方式。</p>

  <p>若装载语句出错，会在这个时候得到一个违例。为了检验驱动程序装载语句是不是能正常工作，请将该语句后面直到catch从句之间的代码暂时设为注释。如果程序运行时未出现违例，表明驱动程序的装载是正确的。</p>

  <p>2. 步骤2：配置数据库</p>

  <p>同样地，我们只限于在32位Windows环境中工作；您可能需要研究一下自己的操作系统，找出适合自己平台的配置方法。</p>

  <p>首先打开控制面板。其中可能有两个图标都含有“ODBC”字样，必须选择那个“32位ODBC”，因为另一个是为了保持与16位软件的向后兼容而设置的，和JDBC混用没有任何结果。双击“32位ODBC”图标后，看到的应该是一个卡片式对话框，上面一排有多个卡片标签，其中包括“用户DSN”、“系统DSN”、“文件DSN”等等。其中，“DSN”代表“数据源名称”（Data Source Name）。它们都与JDBC-ODBC桥有关，但设置数据库时唯一重要的地方“系统DSN”。尽管如此，由于需要测试自己的配置以及创建查询，所以也需要在“文件DSN”中设置自己的数据库。这样便可让Microsoft Query工具（与Microsoft Office配套提供）正确地找到数据库。注意一些软件公司也设计了自己的查询工具。</p>

  <p>最有趣的数据库是我们已经使用过的一个。标准ODBC支持多种文件格式，其中包括由不同公司专用的一些格式，如dBASE。然而，它也包括了简单的“逗号分隔ASCII”格式，它几乎是每种数据工具都能够生成的。就目前的例子来说，我只选择自己的“people”数据库。这是我多年来一直在维护的一个数据库，中间使用了各种联络管理工具。我把它导出成为一个逗号分隔的ASCII文件（一般有个.csv扩展名，用Outlook Express导出通信簿时亦可选用同样的文件格式）。在“文件DSN”区域，我按下“添加”按钮，选择用于控制逗号分隔ASCII文件的文本驱动程序（Microsoft Text Driver），然后撤消对“使用当前目录”的选择，以便导出数据文件时可以自行指定目录。</p>

  <p>大家会注意到在进行这些工作的时候，并没有实际指定一个文件，只是一个目录。那是因为数据库通常是由某个目录下的一系列文件构成的（尽管也可能采用其他形式）。每个文件一般都包含了单个“数据表”，而且SQL语句可以产生从数据库中多个表摘取出来的结果（这叫作“联合”，或者join）只包含了单张表的数据库（就象目前这个）通常叫作“平面文件数据库”。对于大多数问题，如果已经超过了简单的数据存储与获取力所能及的范围，那么必须使用多个数据表。通过“联合”，从而获得希望的结果。我们把这些叫作“关系型”数据库。</p>

  <p>3. 步骤3：测试配置</p>

  <p>为了对配置进行测试，需用一种方式核实数据库是否可由查询它的一个程序“见到”。当然，可以简单地运行上述的JDBC示范程序，并加入下述语句：</p>

  <p>Connection c = DriverManager.getConnection(</p>

  <p>dbUrl, user, password);</p>

  <p>若掷出一个违例，表明你的配置有误。</p>

  <p>然而，此时很有必要使用一个自动化的查询生成工具。我使用的是与Microsoft Office配套提供的Microsoft Query，但你完全可以自行选择一个。查询工具必须知道数据库在什么地方，而Microsoft Query要求我进入ODBC Administrator的“文件DSN”卡片，并在那里新添一个条目。同样指定文本驱动程序以及保存数据库的目录。虽然可将这个条目命名为自己喜欢的任何东西，但最好还是使用与“系统DSN”中相同的名字。</p>

  <p>做完这些工作后，再用查询工具创建一个新查询时，便会发现自己的数据库可以使用了。</p>

  <p>4. 步骤4：建立自己的SQL查询</p>

  <p>我用Microsoft Query创建的查询不仅指出目标数据库存在且次序良好，也会自动生成SQL代码，以便将其插入我自己的Java程序。我希望这个查询能够检查记录中是否存在与启动Java程序时在命令行键入的相同的“姓”（Last Name）。所以作为一个起点，我搜索自己的姓“Eckel”。另外，我希望只显示出有对应E-mail地址的那些名字。创建这个查询的步骤如下：</p>

  <p>(1) 启动一个新查询，并使用查询向导（Query Wizard）。选择“people”数据库（等价于用适应的数据库URL打开数据库连接）。</p>

  <p>(2) 选择数据库中的“people”表。从这张数据表中，选择FIRST，LAST和EMAIL列。</p>

  <p>(3) 在“Filter Data”（过滤器数据库）下，选择LAST，并选择“equals”（等于），加上参数Eckel。点选“And”单选钮。</p>

  <p>(4) 选择EMAIL，并选中“Is not Null”（不为空）。</p>

  <p>(5) 在“Sort By”下，选择FIRST。</p>

  <p>查询结果会向我们展示出是否能得到自己希望的东西。</p>

  <p>现在可以按下SQL按钮。不需要我们任何方面的介入，正确的SQL代码会立即弹现出来，以便我们粘贴和复制。对于这个查询，相应的SQL代码如下：</p>

  <p>SELECT people.FIRST, people.LAST, people.EMAIL</p>

  <p>FROM people.csv people</p>

  <p>WHERE (people.LAST='Eckel') AND</p>

  <p>(people.EMAIL Is Not Null)</p>

  <p>ORDER BY people.FIRST</p>

  <p>若查询比较复杂，手工编码极易出错。但利用一个查询工具，就可以交互式地测试自己的查询，并自动获得正确的代码。事实上，亲手为这些事情编码是难以让人接受的。</p>

  <p>5. 步骤5：在自己的查询中修改和粘贴</p>

  <p>我们注意到上述代码与程序中使用的代码是有所区别的。那是由于查询工具对所有名字都进行了限定，即便涉及的仅有一个数据表（若真的涉及多个数据表，这种限定可避免来自不同表的同名数据列发生冲突）。由于这个查询只需要用到一个数据表，所以可考虑从大多数名字中删除“people”限定符，就象下面这样：</p>

  <p>SELECT FIRST, LAST, EMAIL</p>

  <p>FROM people.csv people</p>

  <p>WHERE (LAST='Eckel') AND</p>

  <p>(EMAIL Is Not Null)</p>

  <p>ORDER BY FIRST</p>

  <p>此外，我们不希望“硬编码”这个程序，从而只能查找一个特定的名字。相反，它应该能查找我们在命令行动态提供的一个名字。所以还要进行必要的修改，并将SQL语句转换成一个动态生成的字串。如下所示：</p>

  <p>"SELECT FIRST, LAST, EMAIL " +</p>

  <p>"FROM people.csv people " +</p>

  <p>"WHERE " +</p>

  <p>"(LAST='" + args[0] + "') " +</p>

  <p>" AND (EMAIL Is Not Null) " +</p>

  <p>"ORDER BY FIRST"</p>

  <div class="mbppagebreak"></div>
</body>
</html>
